contents
Git은 작거나 매우 큰 프로젝트를 빠르고 효율적으로 처리하도록 설계된 무료 오픈소스 **분산 버전 관리 시스템(DVCS)**입니다. 2005년 리눅스 커널을 만든 리누스 토르발스가 개발했으며, 현재는 소스 코드 관리를 위한 전 세계적인 표준이 되었습니다.
Git이 해결하는 문제 (존재 이유)
Git 이전에는 개발자들이 코드 변경 사항을 관리하는 데 상당한 어려움을 겪었습니다.
-
버전 관리의 부재: 프로젝트 폴더를 수동으로 복사하고 이름을 바꾸는 방식(예:
프로젝트_v2,프로젝트_최종,프로젝트_진짜최종). 이 방법은 실수가 잦고 혼란스러우며 확장하기 불가능합니다. -
중앙 집중식 버전 관리 시스템 (CVCS): Subversion(SVN)이나 Perforce 같은 도구들은 큰 발전을 이루었습니다. 이들은 단일 중앙 서버를 사용하여 전체 프로젝트 히스토리를 저장했습니다. 개발자들은 이 서버에서 파일을 "체크아웃"하여 변경하고 "체크인"했습니다.
- 문제점: 이 모델은 단일 실패 지점(single point of failure)을 가집니다. 중앙 서버가 다운되면 아무도 협업하거나 버전을 저장할 수 없습니다. 또한 모든 작업에 네트워크 접근이 필요하므로 원격으로 작업하는 개발자에게는 속도가 느렸습니다.
Git은 분산 시스템이라는 특징으로 이 문제를 해결했습니다. 저장소를 복제(clone)하면 파일의 최신 버전만 가져오는 것이 아니라, 프로젝트의 전체 히스토리를 로컬 머신에 가져옵니다.
핵심 개념: Git의 사고방식 🧠
Git을 제대로 이해하려면 다른 시스템과는 다른 Git의 기본 철학을 이해해야 합니다.
1. 차이가 아닌 스냅샷
대부분의 다른 버전 관리 시스템은 정보를 파일 기반의 변경 사항 목록(델타 또는 diff)으로 저장합니다. Git은 데이터를 다르게 생각합니다. 커밋(commit)(프로젝트 상태를 저장)할 때마다 Git은 기본적으로 그 순간의 모든 파일에 대한 사진, 즉 스냅샷을 찍고 그 스냅샷에 대한 참조를 저장합니다. 효율성을 위해 파일이 변경되지 않았다면 Git은 파일을 다시 저장하지 않고, 이미 저장된 이전의 동일한 파일에 대한 링크만 저장합니다.
2. 세 가지 상태
이는 일상적인 사용에서 가장 중요한 개념입니다. Git에는 파일이 존재할 수 있는 세 가지 주요 상태가 있습니다.
-
Working Directory (작업 디렉터리): 로컬 프로젝트 폴더입니다. 프로젝트의 특정 버전을 하나 체크아웃한 상태이며, 모든 편집 작업을 여기서 합니다.
-
Staging Area (스테이징 영역 또는 인덱스): 다음 커밋에 포함될 내용에 대한 정보를 저장하는 파일입니다. 영구적으로 기록하기 전에 다음 스냅샷을 신중하게 구성할 수 있는 중간 영역입니다.
-
Git Directory (리포지토리): 프로젝트 내의
.git폴더입니다. Git이 프로젝트의 메타데이터와 객체 데이터베이스, 즉 전체 히스토리를 저장하는 곳입니다. Git에서 가장 중요한 부분입니다.
비유: 당신이 전시회를 준비하는 사진작가라고 상상해 보세요.
-
작업실은 모든 사진을 편집하는 작업 디렉터리입니다.
-
스테이징 영역은 전시에 포함할 특정 사진을 선택하고 배열하는 레이아웃 보드입니다. 선택이 마음에 들 때까지 사진을 추가하고 뺄 수 있습니다.
-
리포지토리는 설명과 함께 완성된 전시회(커밋)를 영구적으로 보관하는 갤러리 아카이브입니다.
3. 분산된 특징
모든 개발자의 작업 복사본은 완전한 기능을 갖춘 리포지토리입니다. 이는 완전히 오프라인 상태에서도 새로운 버전을 커밋하고, 히스토리를 보고, 브랜치를 생성할 수 있다는 의미입니다. 다른 사람에게 변경 사항을 푸시하거나 다른 사람의 변경 사항을 풀(pull)할 때만 원격 서버에 연결하면 됩니다. 이는 Git을 엄청나게 빠르고 유연하게 만듭니다.
기본 워크플로우 및 필수 명령어
기본적인 Git 워크플로우는 세 가지 상태를 따릅니다.
-
작업 디렉터리에서 파일을 수정합니다.
-
다음 커밋에 포함할 변경 사항을 선택적으로 골라 스테이징 영역에 추가합니다.
-
커밋을 수행하여 스테이징 영역에 있는 파일들의 스냅샷을 **Git 디렉터리(리포지토리)**에 영구적으로 저장합니다.
이 워크플로우에 해당하는 명령어는 다음과 같습니다.
-
git init: 현재 디렉터리에 새로운 Git 리포지토리를 초기화합니다..git폴더가 생성됩니다. -
git clone [url]: 원격 리포지토리의 로컬 복사본을 만듭니다. -
git add [file]: 새 파일을 추적하거나 수정된 파일을 스테이징합니다. 작업 디렉터리에서 스테이징 영역으로 이동시킵니다. -
git commit -m "설명 메시지": 스테이징 영역의 스냅샷을 리포지토리 히스토리에 영구적으로 저장하는 새 커밋을 만듭니다. 메시지는 변경 이유를 설명하는 데 매우 중요합니다. -
git status: 작업 디렉터리와 스테이징 영역의 현재 상태를 보여주어 어떤 파일이 수정되었는지, 스테이징되었는지, 추적되지 않는지 등을 보여줍니다. -
git log: 리포지토리의 커밋 히스토리를 표시합니다.
브랜치와 병합: Git의 슈퍼파워 🌿
이것은 Git의 가장 중요한 기능이라고 할 수 있습니다.
브랜치란?
Git의 브랜치는 단순히 커밋을 가리키는 가볍고 이동 가능한 포인터입니다. 코드베이스의 복사본이 아닙니다. 새 브랜치를 만들 때 Git은 단지 새 포인터를 생성할 뿐입니다. 이 덕분에 프로젝트 크기에 상관없이 브랜치를 만들고 전환하는 것이 거의 즉각적으로 이루어집니다.
왜 브랜치를 사용하는가?
브랜치를 사용하면 주 개발 라인(main 또는 master)에서 벗어나 새로운 기능, 버그 수정 또는 실험을 독립적으로 진행할 수 있습니다. 작업이 성공적이지 않으면 브랜치를 간단히 버릴 수 있고, 성공적이면 주 브랜치로 **병합(merge)**할 수 있습니다.
필수 브랜치 명령어
-
git branch [branch-name]: 새 브랜치를 생성합니다. -
git checkout [branch-name](또는git switch [branch-name]): 작업 디렉터리를 지정된 브랜치로 전환합니다. -
git merge [branch-name]: 지정된 브랜치의 히스토리를 현재 브랜치로 병합합니다. -
병합 충돌 (Merge Conflicts): 두 브랜치가 동일한 파일의 동일한 부분을 변경한 경우, Git은 어떤 변경 사항을 유지할지 자동으로 결정할 수 없습니다. 이를 병합 충돌이라고 하며, Git은 병합을 일시 중지하고 사용자에게 수동으로 차이점을 해결하도록 요청합니다.
원격 리포지토리 작업 🤝
협업을 하려면 다른 사람들과 작업을 동기화해야 합니다. 이는 원격 리포지토리(주로 GitHub, GitLab, Bitbucket과 같은 서비스에서 호스팅됨)와 상호 작용하여 수행됩니다.
-
git remote add [name] [url]: 로컬 리포지토리를 원격 서버에 연결합니다. 기본 이름은 보통origin입니다. -
git fetch [remote-name]: 원격 리포지토리의 모든 히스토리를 다운로드하지만, 로컬 작업 브랜치에 통합하지는 않습니다. 이를 통해 다른 사람들이 작업한 내용을 확인할 수 있습니다. -
git pull [remote-name] [branch-name]: 원격에서 변경 사항을 가져와서(fetch) 즉시 현재 브랜치로 병합(merge)하려고 시도합니다.git fetch후git merge를 하는 것의 단축 명령어입니다. -
git push [remote-name] [branch-name]: 로컬 브랜치의 커밋을 원격 리포지토리로 업로드하여 다른 사람들과 변경 사항을 공유합니다.
일반적인 워크플로우
팀은 이러한 핵심 기능을 사용하여 워크플로우를 구축합니다. 가장 일반적인 것은 **기능 브랜치 워크플로우(Feature Branch Workflow)**입니다.
-
main브랜치는 항상 안정적이고 배포 가능한 상태로 유지합니다. -
새로운 작업(기능 또는 버그 수정)을 하려면
main에서 새 브랜치를 만듭니다(예:feature/add-login-page). -
이 브랜치에 작업을 커밋합니다.
-
기능이 완성되면 GitHub와 같은 플랫폼에서 "풀 리퀘스트(Pull Request)"(또는 "Merge Request")를 엽니다.
-
팀원들이 코드를 검토하고 승인되면, 기능 브랜치는 다시
main으로 병합됩니다.
결론적으로, Git은 현대 소프트웨어 개발을 위한 견고한 기반을 제공하는 강력하고 다재다능한 도구입니다. 그 속도, 분산된 특징, 강력한 브랜치 기능 덕분에 코드를 작성하는 모든 사람에게 필수적인 기술이 되었습니다.
프로그래밍 및 알고리즘 관점에서 본 Git의 설계는 단순함과 효율성의 정수라고 할 수 있습니다. Git이 속도와 낮은 저장 공간 요구사항을 달성한 것은 복잡한 알고리즘 하나가 아니라, 영리하고 단순한 아이디어들의 조합 덕분입니다.
Git이 어떻게 만들어졌는지에 프로그래밍 관점에서 간단하게 알아보겠습니다.
프로그래밍 언어
Git은 주로 C언어로 작성되었습니다. 리누스 토르발스가 C를 선택한 데에는 몇 가지 핵심적인 이유가 있습니다.
-
속도: C는 매우 효율적인 기계 코드로 컴파일되므로, 리눅스 커널과 같은 거대한 저장소를 처리하는 데 필요한 순수한 성능을 Git에 제공합니다.
-
저수준 제어: C는 직접적인 메모리 접근과 시스템 리소스 제어를 제공하는데, 이는 Git이 수행하는 파일 시스템 중심의 작업에 매우 중요합니다.
-
이식성: C언어와 그 표준 라이브러리는 거의 모든 운영체제에서 사용할 수 있어 Git의 이식성을 매우 높여줍니다(리눅스, macOS, 윈도우 등에서 모두 실행됩니다).
핵심 기능은 C로 작성되었지만, 많은 상위 수준의 "porcelain" 명령어들은 초기에 셸 스크립트로 작성되었습니다. 이를 통해 빠른 프로토타이핑과 개발이 가능했습니다. 시간이 지나면서 이들 중 다수는 더 나은 성능과 플랫폼 간 일관성을 위해 C로 재작성되었습니다.
핵심 데이터 모델: 속도와 단순성의 기반
Git의 기초는 믿을 수 없을 정도로 단순한 키-값(key-value) 데이터 저장소입니다. Git의 핵심에서 모든 것은 객체(object) 이며, 전체 시스템은 단 네 가지 객체 유형(블롭, 트리, 커밋, 태그) 위에 구축됩니다. 이 시스템의 핵심은 객체가 저장되고 참조되는 방식에 있습니다.
1. 콘텐츠 주소 지정 방식 저장소 (비밀 병기)
이것이 Git 설계의 가장 뛰어난 부분이자 대부분의 장점이 나오는 원천입니다. Git이 어떤 콘텐츠(파일 데이터, 디렉터리 구조, 커밋 등)를 저장할 때, 무작위 이름을 부여하지 않습니다. 대신 다음과 같이 작동합니다.
-
객체 유형을 식별하는 작은 헤더를 추가합니다(예: "blob").
-
이 헤더와 콘텐츠를 SHA-1 해싱 알고리즘에 통과시킵니다.
-
결과로 나오는 40자리의 16진수 문자열이 바로 그 객체의 이름이자 키가 됩니다.
이러한 콘텐츠 주소 지정 방식은 엄청난 이점을 가집니다.
-
데이터 무결성: SHA-1 해시는 체크섬(checksum) 역할을 합니다. 디스크 손상으로 비트 하나라도 바뀌면, 해시를 다시 계산했을 때 다른 결과가 나오므로 Git은 객체가 손상되었음을 알 수 있습니다.
-
자동 중복 제거: 프로젝트 히스토리의 서로 다른 열 군데에 동일한 파일 콘텐츠가 있더라도, Git은 매번 동일한 SHA-1 해시를 계산합니다. 결과적으로 그 콘텐츠를 객체 데이터베이스(
.git/objects)에 단 한 번만 저장합니다. 모든 트리와 커밋은 그 단일 객체를 가리킬 뿐입니다. 이는 엄청난 저장 공간 절약 효과를 가져옵니다. 변경 없이 50번 커밋된 100MB짜리 동영상 파일은 5GB가 아닌 100MB의 공간만 차지합니다. -
빠른 조회: SHA-1 해시로 객체를 찾는 것은 파일 시스템이 파일 이름으로 파일을 빠르게 찾을 수 있기 때문에 매우 빠릅니다.
2. 단순한 객체 그래프
-
블롭 (Blob - Binary Large Object): 이 객체는 단순히 파일의 원본 콘텐츠를 저장합니다. 파일 이름이나 위치에 대해서는 아무것도 모르며, 그저 데이터 덩어리일 뿐입니다.
-
트리 (Tree): 이 객체는 디렉터리를 나타냅니다. 포인터 목록을 포함하며, 각 포인터는 타입(블롭 또는 트리), 가리키는 객체의 SHA-1 해시, 그리고 파일명이나 디렉터리명을 가집니다. Git이 프로젝트 디렉터리 구조의 스냅샷을 재구성하는 방식입니다.
-
커밋 (Commit): 이 객체는 단일 트리 객체(그 순간의 프로젝트 루트)를 가리킵니다. 또한 부모 커밋의 SHA-1 해시, 작성자, 커밋한 사람, 커밋 메시지와 같은 메타데이터를 포함합니다. 이 부모 포인터들이 바로 프로젝트의 히스토리인 역사적 체인(방향성 비순환 그래프)을 만듭니다.
이 객체들은 단순하고 불변(immutable, 절대 변하지 않음)이기 때문에 시스템을 이해하고 작업을 수행하는 것이 매우 간단해집니다.
데이터 저장 효율성: 팩파일(Packfiles)
.git/objects 디렉터리에 수백만 개의 작은 객체 파일("느슨한 객체")을 두는 것은 대부분의 파일 시스템에서 매우 비효율적입니다. 이 문제를 해결하기 위해 Git은 팩파일을 사용합니다.
주기적으로(또는 push/pull 시) Git은 많은 느슨한 객체들을 팩파일(.pack) 이라는 고도로 압축된 단일 파일로 묶습니다. 이는 ZIP 파일을 만드는 것과 비슷합니다.
하지만 훨씬 더 영리한 일을 합니다: 델타 압축(Delta Compression).
패킹 과정에서 Git은 유사한 파일들을 찾습니다(예: 동일한 소스 코드 파일의 두 버전). 두 파일 전체를 저장하는 대신, 한 버전은 완전한 "베이스" 객체로 저장하고 다른 버전은 베이스와의 차이점, 즉 델타만 저장합니다.
이것이 Git이 점진적으로 변경되는 텍스트 기반 파일을 매우 효율적으로 저장하는 이유입니다. Git은 사용자 모델 관점에서는 스냅샷으로 생각하지만, 저장 모델 관점에서는 델타를 사용합니다. (전자는 이해하기 쉽고, 후자는 효율적입니다.)
이 큰 팩파일 안에서도 객체를 빠르게 찾을 수 있도록, Git은 객체의 SHA-1 해시를 팩파일 내의 바이트 오프셋에 매핑하는 인덱스 파일(.idx)도 함께 생성합니다.
브랜칭과 병합이 빠른 이유
-
브랜칭: 브랜치는 코드베이스의 복사본이 아닙니다. Git에서 브랜치는 말 그대로
.git/refs/heads/디렉터리에 있는 단순한 파일입니다. 이 파일에는 단 하나, 브랜치가 가리키는 커밋의 40자리 SHA-1 해시만 들어있습니다. 브랜치를 만드는 것은 파일에 40자를 쓰는 것만큼 빠릅니다. 브랜치를 전환하는 것은HEAD파일(또 다른 단순한 포인터)을 업데이트하고 작업 디렉터리의 파일들을 스냅샷과 일치시키는 과정입니다. -
병합: Git은 일반적으로 3-방향 병합(three-way merge) 알고리즘을 사용합니다. 두 브랜치를 병합할 때, 단지 두 끝점만 보는 것이 아니라, 두 브랜치의 가장 최근 공통 조상을 찾습니다. 그런 다음 그 공통 조상을 기준으로 두 브랜치에서 각각 만들어진 변경 사항을 비교합니다.
-
만약 브랜치 A가 파일의 1번 부분을 변경하고 브랜치 B가 2번 부분을 변경했다면, Git은 이를 자동으로 병합할 수 있습니다.
-
만약 두 브랜치가 파일의 동일한 부분을 변경했다면, 사용자가 해결해야 할 병합 충돌이 발생합니다.
-
요약하자면, Git의 천재성은 단순함에 있습니다. SHA-1 해싱을 이용한 콘텐츠 주소 지정 방식의 키-값 저장소, 단순하고 불변인 객체 그래프, 그리고 델타 압축된 팩파일과 같은 영리한 저장 최적화 등 몇 가지 근본적이고 강력한 프로그래밍 개념 위에 구축되었습니다. 이 기반이 바로 Git을 그토록 빠르고, 효율적이며, 신뢰할 수 있게 만드는 것입니다.
references